#include "tracticketexplorerbackend.h"

#include "ticket.h"
#include "ticket_p.h"
#include "ticketchangeset.h"
#include "ticketexplorer.h"
#include "ticketitemmodel.h"
#include "ticketquery.h"

#include "xmlrpc/xmlrpcconnection.h"
#include "xmlrpc/xmlrpcrequest.h"
#include "xmlrpc/xmlrpcresponse.h"

#include <coreplugin/icore.h>

#include <QtCore/QUrl>
#include <QtCore/QDebug>
#include <QtCore/QList>
#include <QtCore/QTextStream>
#include <QtCore/QSettings>
#include <QtGui/QPainter>
#include <QtGui/QAbstractItemView>


TracTicketListDelegate::TracTicketListDelegate(QObject *parent)
    : QStyledItemDelegate(parent)
{

}

TracTicketListDelegate::~TracTicketListDelegate()
{
}

QSize TracTicketListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QStyleOptionViewItemV4 opt4 = option;
    initStyleOption(&opt4, index);

    QFontMetrics fm(opt4.font);
    int fontHeight = fm.height();

    QSize size;
    size.setWidth(opt4.rect.width());
    size.setHeight(fontHeight + 15);
    // TODO: Check min height
    return size;
}

inline static QString getElidedText(const QAbstractItemModel *model, const QModelIndex &index,
                             int role, const QFontMetrics &fm, const QRect &r)
{
    return fm.elidedText(model->data(index, role).toString(), Qt::ElideRight, r.width());
}

void TracTicketListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
                                   const QModelIndex &index) const
{
    painter->save();

    QStyleOptionViewItemV4 opt4 = option;
    initStyleOption(&opt4, index);

    const QAbstractItemView *view = qobject_cast<const QAbstractItemView *>(opt4.widget);
    bool selected = view->selectionModel()->currentIndex().row() == index.row();

    QColor bgColor;
    bool odd =  index.row() % 2;

    if (selected)
        bgColor = opt4.palette.highlight().color();
    else
        bgColor = (odd) ? opt4.palette.background().color() : QColor("white");

    painter->setBrush(bgColor);
    painter->setPen(Qt::NoPen);
    painter->drawRect(opt4.rect);

    const QAbstractItemModel *model = view->model();

    QRect b = opt4.rect;
    b.adjust(2, 2, -4, -4);

    painter->setBrush(Qt::NoBrush);
    painter->setPen(opt4.palette.foreground().color());
    QFontMetrics fm(painter->font());

    // An awful lot of repetition here, fixy fix..
    QString text;
    switch (index.column()) {
    case 0:
        text = QString("#%1").arg(model->data(index, BaseTicketItemModel::Id).toString());
        painter->drawText(b, Qt::TextSingleLine | Qt::AlignHCenter | Qt::AlignTop, text);
        break;
    case 1:
    {
        text = getElidedText(model, index, BaseTicketItemModel::Summary, fm, b);
        painter->drawText(b, Qt::TextSingleLine | Qt::AlignLeft | Qt::AlignTop, text);
        break;
    }
    case 2:
        painter->drawText(b, Qt::TextSingleLine | Qt::AlignLeft | Qt::AlignVCenter,
                          model->data(index, BaseTicketItemModel::Status).toString().toUpper());
        break;
    case 3:
        text = getElidedText(model, index, BaseTicketItemModel::Assignee, fm, b);
        painter->drawText(b, Qt::TextSingleLine | Qt::AlignLeft | Qt::AlignVCenter, text);
        break;
    case 4:
        text = getElidedText(model, index, BaseTicketItemModel::Type, fm, b);
        painter->drawText(b, Qt::TextSingleLine | Qt::AlignLeft | Qt::AlignVCenter, text);
        break;
    case 5:
    {
        text = model->data(index, BaseTicketItemModel::Priority).toString();
        painter->setBrush(QColor(0xb5, 0xb5, 0xb5));
        painter->setPen(Qt::NoPen);
        painter->drawRect(b.adjusted(0, 2, 0, -2));
        painter->setPen(QColor("white"));
        QFont font = painter->font();
        font.setCapitalization(QFont::AllUppercase);
        painter->setFont(font);
        painter->drawText(b.adjusted(2, 2, -2, -2),
                          Qt::TextSingleLine | Qt::AlignHCenter | Qt::AlignVCenter, text);

        break;
    }
    case 6:
        QDateTime dt = model->data(index, BaseTicketItemModel::Created).toDateTime();
        painter->drawText(b, Qt::TextSingleLine | Qt::AlignLeft | Qt::AlignVCenter, dt.toString());
        break;
    }

    painter->restore();
}

TracTicketExplorerBackend::TracTicketExplorerBackend(ITicketBackendConfiguration *config,
                                                     QObject *parent)
    : ITicketExplorerBackend(config, parent),
      m_con(new XmlRpc::XmlRpcConnection()), m_queryId(0), m_queryModel(0),
      m_ticketQueryCount(0), m_totalTickets(0)
{
    TracTicketConfiguration *conf = qobject_cast<TracTicketConfiguration *>(m_config);
    if (conf) {
        m_con->setUrl(conf->url());
        m_con->setUsername(conf->username());
        m_con->setPassword(conf->password());
    }
    connect(m_con, SIGNAL(finished(XmlRpc::XmlRpcResponse*)),
            this, SLOT(rpcCallComplete(XmlRpc::XmlRpcResponse*)));
}

TracTicketExplorerBackend::~TracTicketExplorerBackend()
{
    delete m_con;
}

QString TracTicketExplorerBackend::id() const
{
    return "Trac XML-RPC";
}

static QString opToStr(TicketQuery::Operator op)
{
    switch (op) {
    case TicketQuery::OperatorIs:
        return "=";
    case TicketQuery::OperatorContains:
        return "=~";
    case TicketQuery::OperatorIsNot:
        return "!=";
    }
    return "=~";
}

static QString buildQuery(const TicketQuery &query)
{
    QStringList searchTerms;

    QString searchStr = query.searchStr();
    QString owner = query.owner();
    if (!owner.isEmpty())
        searchTerms.append(QString("owner=~%1").arg(owner));
    TicketQuery::QueryFieldMap fields = query.fields();
    foreach (QString field, fields.keys()) {
        TicketQuery::QuerySpec spec = fields.value(field);
        searchTerms.append(QString("%1%2%3").arg(field, opToStr(spec.first), spec.second));
    }
    QStringList statuses = query.statuses();
    foreach (QString field, statuses)
        searchTerms.append(QString("status=%1").arg(field));
    return (query.type() == TicketQuery::Basic) ? searchTerms.join("&or&") : searchTerms.join("&");
}

BaseTicketItemModel *TracTicketExplorerBackend::queryModel(TicketQuery query)
{
    // Start new query. We only allow one ongoing ticket list query.
    m_queryId++;
    m_queryModel = new BaseTicketItemModel(QList<Ticket *>());
    m_ticketQueryCount = 0;
    XmlRpc::XmlRpcRequest request("ticket.query");
    QString queryStr = buildQuery(query);
    if (!queryStr.isEmpty())
        request.setParams(QList<QVariant>() << queryStr);
    m_requests.insert(m_con->query(request), TracQuery(m_queryId, ListRequest, 0));
    return m_queryModel;
}


static TicketError makeError(TicketError::ErrorType type, const QString &text,
                             XmlRpc::XmlRpcResponse *response, const Ticket *ticket = 0)
{
    QString err;
    // We do some magic here to hide the username and password from network replies
    if (response->error() == XmlRpc::XmlRpcResponse::NetworkError)
        err = QString("network error %1").arg(response->errorCode());
    else
        err = response->errorString();
    return TicketError(type, QString("%1: %2").arg(text).arg(err), response->errorCode(), ticket);
}

void TracTicketExplorerBackend::rpcCallComplete(XmlRpc::XmlRpcResponse *response)
{
    if (!m_requests.contains(response))
        return;

    TracQuery query(m_requests.value(response));
    bool isError = (response->error() != XmlRpc::XmlRpcResponse::NoError);
    QVariant ret = response->returnValue();

    switch (query.type) {
    case CreateTicketRequest:
        if (!isError) {
            TicketPrivate *t = qobject_cast<TicketPrivate *>(query.ticket);
            if (t) {
                t->setId(t->id());
                t->doUpdateNotify();
            }
        } else {
            sendError(makeError(TicketError::ErrorTicketCreateFailed,
                              "Failed to create ticket", response, query.ticket));
        }
        break;
    case ListRequest:
        if (!isError && query.queryId == m_queryId && ret.canConvert(QVariant::List)) {
            QVariantList ids = ret.toList();
            foreach (QVariant id, ids)
                if (id.canConvert(QVariant::Int))
                    queryTicket(id.toInt());
            m_totalTickets = m_ticketQueryCount;
            if (m_totalTickets == 0)
                m_queryModel->notifyQueryCompleted(0, 0);
        } else if (isError) {
            sendError(makeError(TicketError::ErrorUnableToListTickets,
                                        "Unable to retrieve ticket list", response));
        }
        break;
    case ListSingleRequest:
        if (query.queryId == m_queryId) {
            --m_ticketQueryCount;
            m_queryModel->notifyQueryCompleted(m_totalTickets - m_ticketQueryCount, m_totalTickets);
        }

        if (!isError && query.queryId == m_queryId) {
            Ticket *ticket = ticketFromVariant(ret);
            if (ticket && !m_queryModel.isNull())
                m_queryModel->addTicket(ticket);
        } else if (isError) {
            sendError(makeError(TicketError::ErrorCouldNotLoadTicket,
                                        "Unable to retrieve ticket", response, query.ticket));
        }
        break;
    case FieldsRequest:
        doFieldsUpdate(response);
        break;
    case HistoryRequest:
        doHistoryUpdate(query, response);
        break;
    case ActionsRequest:
        doActionsUpdate(query, response);
        break;
    case UpdateRequest:
        if (!isError) {
            TicketPrivate *ticket = qobject_cast<TicketPrivate *>(query.ticket);
            if (ticket)
                updateTicketFromVariant(ticket, ret);
        } else {
            sendError(makeError(TicketError::ErrorFailedToUpdateTicket,
                                        "Unable to update ticket", response, query.ticket));
        }
        break;
    case RecentChangesRequest:
        if (!isError) {
            if (ret.canConvert(QVariant::List)) {
                QVariantList varList = ret.toList();
                QList<int> ids;
                foreach (QVariant var, varList)
                    if (var.canConvert(QVariant::Int))
                        ids << var.toInt();
                emit changedTickets(ids);
            }
        } else {
            sendError(makeError(TicketError::ErrorFailedToRetrieveRecentChanges,
                                        "Unable to retrieve recent changes", response));
        }
    default:
        break;
    }

    response->deleteLater();
}

bool TracTicketExplorerBackend::updateTicketFromVariant(Ticket *ticket, const QVariant &val)
{
    if (!val.canConvert(QVariant::List))
        return false;

    QVariantList ticketData = val.toList();
    if (ticketData.length() != 4) {
        qWarning() << "Invalid ticket data in response";
        return false;
    }

    QScopedPointer<TicketEditor> editor(ticket->editor());
    editor->setId(ticketData.at(0).toInt());
    editor->setCreated(ticketData.at(1).toDateTime());
    editor->setModified(ticketData.at(2).toDateTime());

    QVariantMap attribs = ticketData.at(3).toMap();
    foreach (QString attrib, attribs.keys())
        editor->setFieldByName(attrib, attribs.value(attrib));
    editor->update();
    return true;
}

Ticket *TracTicketExplorerBackend::ticketFromVariant(const QVariant &val)
{
    TicketPrivate *ticket = new TicketPrivate(new TicketExplorerBackendContext(this));
    if (!updateTicketFromVariant(ticket, val)) {
        delete ticket;
        ticket = 0;
        sendError(TicketError(TicketError::ErrorBackendBadData, "Invalid ticket response"));
    }
    return ticket;
}

void TracTicketExplorerBackend::queryTicket(int ticketId)
{
    XmlRpc::XmlRpcRequest request("ticket.get");
    request.setParams(QVariantList() << ticketId);
    m_requests.insert(m_con->query(request), TracQuery(m_queryId, ListSingleRequest, 0));
    m_ticketQueryCount++;
}

Ticket *TracTicketExplorerBackend::createTicket()
{
    Ticket *ticket = new TicketPrivate(new TicketExplorerBackendContext(this));
    return ticket;
}

void TracTicketExplorerBackend::storeTicket(Ticket *ticket, const QString &comment)
{
    Q_ASSERT(ticket != 0);

    TicketPrivate *t = qobject_cast<TicketPrivate *>(ticket);
    if (t == 0)
        return;

    // If this is a new ticket, send ticket.create, otherwise ticket.update
    if (t->id() == -1)
        sendCreateTicket(t);
    else
        sendUpdateTicket(t, comment);
}

static TicketChangeset *findChangeset(const QList<TicketChangeset *> &changesets,
                                      const QDateTime &timestamp, const QString &author)
{
    QList<TicketChangeset *>::ConstIterator i = changesets.constBegin();
    for (; i != changesets.constEnd(); ++i)
        if ((*i)->timestamp() == timestamp && (*i)->author() == author)
            return *i;
    return 0;
}

static void parseChangeset(QList<TicketChangeset *> &changesets, const QVariant &value)
{
    // Some sanity checking for the return value
    if (!value.canConvert(QVariant::List))
        return;
    QVariantList csItems = value.toList();
    if (csItems.count() < 6)
        return;
    QDateTime timestamp = csItems.at(0).toDateTime();
    QString author = csItems.at(1).toString();
    QString field = csItems.at(2).toString();
    QString oldValue = csItems.at(3).toString();
    QVariant newValue = csItems.at(4);

    // Check if we already have related changes
    TicketChangeset *cs = findChangeset(changesets, timestamp, author);

    // If not, create a new changeset
    if (!cs) {
        cs = new TicketChangeset(timestamp, author);
        changesets.append(cs);
    }
    cs->addField(field, newValue, oldValue);
}

void TracTicketExplorerBackend::loadHistory(Ticket *ticket)
{
    XmlRpc::XmlRpcRequest request("ticket.changeLog");
    request.setParams(QVariantList() << ticket->id());
    m_requests.insert(m_con->query(request), TracQuery(0, HistoryRequest, ticket));
}

void TracTicketExplorerBackend::loadAvailableActions(Ticket *ticket)
{
    XmlRpc::XmlRpcRequest request("ticket.getActions", QVariantList() << ticket->id());
    m_requests.insert(m_con->query(request), TracQuery(0, ActionsRequest, ticket));
}

void TracTicketExplorerBackend::doHistoryUpdate(const TracQuery &query,
                                                XmlRpc::XmlRpcResponse *response)
{
    if (response->error()) {
        sendError(makeError(TicketError::ErrorFailedToLoadHistory, "Unable to load ticket history",
                            response, query.ticket));
        return;
    }

    TicketPrivate *t = qobject_cast<TicketPrivate *>(query.ticket);

    if (!t)
        return;

    QList<TicketChangeset *> changesets;
    QVariant ret = response->returnValue();
    if (ret.canConvert(QVariant::List)) {
        QVariantList varCsList = ret.toList();
        foreach (QVariant varCs, varCsList)
            parseChangeset(changesets, varCs);
    }
    t->setHistory(changesets);
}

static bool parseTicketField(QVariant value, TicketField *ticketField)
{
    if (value.canConvert(QVariant::Map)) {
        QVariantMap fieldSpec = value.toMap();
        ticketField->name = fieldSpec.value("name", "").toString();
        ticketField->label = fieldSpec.value("label", "").toString();
        ticketField->defaultValue = fieldSpec.value("value", "").toString();
        ticketField->type = fieldSpec.value("type", "").toString();
        if (fieldSpec.contains("options")
                && fieldSpec.value("options").canConvert(QVariant::StringList))
            ticketField->options = fieldSpec.value("options").toStringList();
        return true;
    }
    return false;
}

void TracTicketExplorerBackend::doFieldsUpdate(XmlRpc::XmlRpcResponse *response)
{
    if (response->error()) {
        sendError(makeError(TicketError::ErrorFailedToLoadFields, "Unable to load ticket fields",
                            response));
        return;
    }

    m_ticketFields.clear();
    QVariant ret = response->returnValue();
    if (ret.canConvert(QVariant::List)) {
        QVariantList varFieldList = ret.toList();
        foreach (QVariant value, varFieldList) {
            TicketField t;
            if (parseTicketField(value, &t))
                m_ticketFields << t;
        }
    }
    qSort(m_ticketFields.begin(), m_ticketFields.end());
    emit fieldsUpdated();
}

void TracTicketExplorerBackend::updateFields()
{
    XmlRpc::XmlRpcRequest request("ticket.getTicketFields");
    m_requests.insert(m_con->query(request), TracQuery(0, FieldsRequest, 0));
}

QList<TicketField> TracTicketExplorerBackend::getAvailableFields()
{
    return m_ticketFields;
}

QAbstractItemDelegate *TracTicketExplorerBackend::listDelegate() const
{
    TracTicketListDelegate *delegate = new TracTicketListDelegate();
    return delegate;
}

static TicketAction *ActionfromVariant(QVariant var)
{
    if (!var.canConvert(QVariant::List))
        return 0;

    QVariantList fields = var.toList();
    if (fields.count() != 4)
        return 0;

    // Trac sends empty hint as ".", we "fix" it here.
    QString hint = fields.at(2).toString();
    if (hint == ".")
        hint = "";
    TicketAction *action = new TicketAction(fields.at(0).toString(), fields.at(1).toString(),
                              hint);
    if (fields.at(3).canConvert(QVariant::List)) {
        QVariantList inputFields = fields.at(3).toList();
        // The third argument is a list of 1 entry.. that contains the actual list.
        // Go figure.
        if (inputFields.count() > 0 && inputFields.at(0).canConvert(QVariant::List)
                && inputFields.at(0).toList().count() > 0) {
            inputFields = inputFields.at(0).toList();
            if (inputFields.count() == 3) {
                action->setInputFieldName(inputFields.at(0).toString());
                action->setInputFieldValue(inputFields.at(1).toString());
                if (inputFields.at(2).canConvert(QVariant::List))
                    action->setAllowedFields(inputFields.at(2).toStringList());
            }
        }
    }
    return action;
}


void TracTicketExplorerBackend::doActionsUpdate(const TracQuery &query,
                                                XmlRpc::XmlRpcResponse *response)
{
    if (response->error()) {
        sendError(makeError(TicketError::ErrorFailedToLoadActions, "Failed to update ticket actions",
                            response, query.ticket));
        return;
    }

    TicketPrivate *t = qobject_cast<TicketPrivate *>(query.ticket);
    if (!t)
        return;

    QList<TicketAction *> actions;

    QVariant ret = response->returnValue();
    if (ret.canConvert(QVariant::List)) {
        QVariantList varActionList = ret.toList();
        foreach (QVariant varAction, varActionList) {
            TicketAction *action = ActionfromVariant(varAction);
            if (action)
                actions << action;
        }
        t->setAvailableActions(actions);
    }
}

void TracTicketExplorerBackend::loadChangedTickets(const QDateTime &since)
{
    XmlRpc::XmlRpcRequest request("ticket.getRecentChanges", QList<QVariant>() << since);
    m_requests.insert(m_con->query(request), TracQuery(0, RecentChangesRequest, 0));
}

void TracTicketExplorerBackend::sendUpdateTicket(TicketPrivate *ticket, const QString &comment)
{
    XmlRpc::XmlRpcRequest request("ticket.update");
    QVariantList params;
    params << ticket->id();
    params << comment;
    QVariantMap fields = ticket->fields();
    fields.remove("time");
    fields.remove("changetime");
    params << fields;
    request.setParams(params);
    m_requests.insert(m_con->query(request), TracQuery(0, UpdateRequest, ticket));
}

void TracTicketExplorerBackend::sendCreateTicket(TicketPrivate *ticket)
{
    XmlRpc::XmlRpcRequest request("ticket.create");
    QVariantList params;

    QVariantMap fields = ticket->fields();
    QString summary = fields.take("summary").toString();
    QString description = fields.take("description").toString();
    params << summary;
    params << description;
    params << fields;
    request.setParams(params);
    m_requests.insert(m_con->query(request), TracQuery(0, CreateTicketRequest, ticket));
}

void TracTicketExplorerBackend::sendError(const TicketError &err)
{
    qWarning() << err;
    emit error(err);
}

void TracTicketExplorerBackend::refreshTicket(Ticket *ticket)
{
    XmlRpc::XmlRpcRequest request("ticket.get");
    request.setParams(QVariantList() << ticket->id());
    m_requests.insert(m_con->query(request), TracQuery(0, UpdateRequest, ticket));
}

QUrl TracTicketExplorerBackend::url() const
{
    TracTicketConfiguration *conf = qobject_cast<TracTicketConfiguration *>(m_config);
    if (conf)
        return conf->url();
    else
        return QUrl();
}

QString TracTicketExplorerBackend::username() const
{
    TracTicketConfiguration *conf = qobject_cast<TracTicketConfiguration *>(m_config);
    if (conf)
        return conf->username();
    else
        return "";
}

void TracTicketConfiguration::readSettings(const QSettings &settings)
{
    m_url = settings.value("url", "").toUrl();
    m_username = settings.value("username", "").toString();
    m_password = settings.value("password", "").toString();
}

void TracTicketConfiguration::writeSettings(QSettings &settings)
{
    Q_UNUSED(settings);
}

QString TracTicketConfiguration::id() const
{
    return "trac";
}


